/*
 *  Copyright (C) 2011  Clint Priest
 *
 *  This file is part of pklib.
 *
 *  pklib is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  pklib is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with pklib.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Portions borrowed or re-written from prototype
 */

function $() {
	var elem = arguments[0];
	if(typeof elem == 'string') {
		elem = document.getElementById(arguments[0]);
		if(elem == null)
			elem = document.getElementById('content').contentDocument.getElementById(arguments[0]);
		if(elem == null)
			return null;
	}
	return ElementExtensions.extend(elem);
}

var ElementExtensions = {
	extend: function(arg) {
		if(Object.isArray(arg)) {
			for(var i=0, x; x = arg[i]; i++)
				x = ElementExtensions.extend(x);
			return arg;
		}
		if(!arg._extendedByPklib) {
			arg = Object.extend(arg, ElementExtensions);
			arg._extendedByPklib = true;
		}
		return arg;
	},
	
	remove: function() {
		if(this.parentNode)
			this.parentNode.removeChild(this);
	},
	
	recursivelyCollect: function(property) {
		var element = this;
		var elements = [];
		while (element = element[property]) {
			if (element.nodeType == 1)
				elements.push(element);
		}
		return $(elements);
	},

	ancestors: function() {
		return this.recursivelyCollect('parentNode');
	},

	descendants: function() {
		return this.select('*');
	},

	immediateDescendants: function() {
		var element = this;
		if (!(element = element.firstChild))
			return [];
		while (element && element.nodeType != 1)
			element = element.nextSibling;
		if (element)
			return $([element].concat($(element).nextSiblings()));
		return [];
	},

	select: function() {
		var element = this;
		var args = Array.prototype.slice.call(arguments, 0);
		return $(args.reduce(function(elements, expression) {
			return elements.concat.apply(elements, element.querySelectorAll(expression));
		}, [ ] ));
	},

	firstDescendant: function() {
		var element = this.firstChild;
		while (element && element.nodeType != 1)
			element = element.nextSibling;
		return $(element);
	},

	previousSiblings: function() {
		return this.recursivelyCollect('previousSibling');
	},

	nextSiblings: function() {
		return this.recursivelyCollect('nextSibling');
	},

	siblings: function() {
		return $(this.previousSiblings().reverse()
			.concat(this.nextSiblings()));
	},
	
	up: function(expression, index) {
		if (arguments.length == 0)
			return $(this.parentNode);
		var ancestors = this.ancestors();
		return Object.isNumber(expression)
			? ancestors[expression]
			: Selector.findElement(ancestors, expression, index);
	},

	down: function(expression, index) {
		if (arguments.length == 0)
			return this.firstDescendant();
		return Object.isNumber(expression)
			? this.descendants()[expression]
			: this.select(expression)[index || 0];
	},

	previous: function(expression, index) {
		if (arguments.length == 0)
			return $(Selector.handlers.previousElementSibling(this));
		var previousSiblings = this.previousSiblings();
		return Object.isNumber(expression)
			? previousSiblings[expression] 
			: Selector.findElement(previousSiblings, expression, index);
	},

	next: function(expression, index) {
		if (arguments.length == 0)
			return $(Selector.handlers.nextElementSibling(this));
		var nextSiblings = this.nextSiblings();
		return Object.isNumber(expression)
			? nextSiblings[expression] 
			: Selector.findElement(nextSiblings, expression, index);
	},
	insert: function(arg) {
		var Content = arg;
		var InsertAt = 'bottom';
		
		if(!Object.isString(arg)) {
			InsertAt = Object.keys(arg)[0];
			Content = arg[Method];
		}
		Content = this.GetContentFromAnonymousElement(Content);
		switch(InsertAt) {
			case 'before':
				this.parentNode.insertBefore(Content, this);
				return this.previousSibling;
			case 'after':
				this.parentNode.insertBefore(Content, this.nextSibling);
				return this.nextSibling;
			case 'top':
				this.parentNode.insertBefore(Content, this.parentNode.childNodes[0]);
				return this.parentNode.childNodes[0];
			case 'bottom':
			default:
				this.appendChild(Content);
				return this.immediateDescendants().last();
		}
	},
	identify: function() {
		var id = this.getAttribute('id');
		if (id)
			return id;
		do { 
			id = 'id'+Math.floor(Math.random() * 10000000);
		} while (document.getElementById(id));
		this.setAttribute('id', id);
		return id;
	},
	/* Animates a series of style changes from the current style to the EndStyle
		* Presently only works with numeric fields (opacity, etc) */
	animate: function(EndStyle, ms) {
		var StartTime = (new Date()).getTime();
		var cs = window.getComputedStyle(this);
		var StartStyle = Object.keys(EndStyle).reduce( function(acc, Key) {
			acc[Key] = cs[Key];
			return acc;
		}, { } );
//		console.log('%s to %s over %s', StartStyle.opacity, EndStyle.opacity, ms);
		clearInterval(this.AnimateIntervalID);
		this.AnimateIntervalID = setInterval(function() {
			var Frame = Math.min((new Date()).getTime() - StartTime, ms) / ms;
			Object.keys(EndStyle).forEach( function(Key) {
				var x = Math.abs((StartStyle[Key] - EndStyle[Key]) * Frame);
				if(StartStyle[Key] >= EndStyle[Key])
					x = StartStyle[Key] - x;
				this.style[Key] = x;
//				console.log('%s, %s', Math.round(Frame*1000)/1000, Math.round((x*1000)/1000));
			}, this);

			if(Frame == 1)
				clearInterval(this.AnimateIntervalID);
		}.bind(this), Math.max(20, ms/100));
	},
	/* Converts a string to a series of elements by way of an anonymous div on this.ownerDocument */
	GetContentFromAnonymousElement: function(Content) {
		if(Object.isString(Content)) {
			var AnonymousElement = this.ownerDocument.createElement('div');
			AnonymousElement.innerHTML = Content;
			Content = AnonymousElement.firstChild;
			AnonymousElement.removeChild(AnonymousElement.firstChild);
		}
		return $(Content);
	},
	/* Applies the given style to the given element returns a hash of what changed styles were originally */
	ApplyStyle: function() {
		var OriginalStyle = { };
		$A(arguments).forEach( function(style) {
			Object.keys(style || {} ).forEach( function(name) {
				OriginalStyle[name] = this.style[name];
				this.style[name] = style[name];
			}, this );
		}, this);
		return OriginalStyle;
	},
	/** Returns an array of { top, left, width, height } objects which combined make up the bounding rects of the given element,
	* 	this uses the built-in .getClientRects() and additionally compensates for 'block' elements which it would appear is not
	* 	handled appropriately for our needs by Mozilla or the standard
	*/
	GetOffsetElementRects: function(offset) {
		offset = offset || { x: 0, y: 0 };

		var Rects = $A(this.getClientRects());

		if(this.tagName == 'A') {
			$A(this.querySelectorAll('IMG')).forEach( function(elem) {
				Rects = Rects.concat( $A(elem.getClientRects()) );
			} );
		}
		return Rects.map( function(rect) {
			return { 	top		: rect.top + offset.y,
						left	: rect.left + offset.x,
						bottom	: rect.top + rect.height + offset.y,
						right	: rect.left + rect.width + offset.x };
		} );
	},
	/* Parses the current style attribute, alters according to NewStyles { } and sets the style attribute again */
	AlterStyleAttribute: function(NewStyles) {
		var CurStyle = this.getAttribute('style').split(';')
			.reduce( function(acc, x) {
				if(x.length) {
					var s = x.split(':');
					acc[s[0].replace(/(^\s*|\s*$)/, '')] = s[1].replace(/(^\s*|\s*$)/, '');
				}
				return acc;
			}, { } );
		Object.keys(NewStyles).forEach( function(Name) {
			CurStyle[Name.replace(/([A-Z])/g, '-$1').toLowerCase()] = NewStyles[Name];
		});
		var Style = [];
		Object.keys(CurStyle).forEach( function(Name) {
			Style.push(Name+': '+CurStyle[Name]+';');
		} );
		this.setAttribute('style', Style.join(' '));
	}
};
